package net.genebase.genome;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import net.genebase.elements.IntegerElement;
import net.genebase.elements.Mutatable;
import net.genebase.genome.exceptions.GenomeSizeMismatchException;
import net.genebase.genome.exceptions.TooManyCrossesException;


/**
 * @param DataType the object type which this Genome uses as it's building blocks. 
 * @author Daniel Miller
 * @version $Revision: 1.0 $
 */
public abstract class Genome<DataType>{
	/**
	 * The name of this Genome object.
	 */
	private String name;
	/**
	 * The List of Mutatable objects which make up this Genome.
	 */
	private List<Mutatable<DataType>> elements = new ArrayList<Mutatable<DataType>>();
	
	/**
	 * This method creates a new instance of a particular Genome given a list of Mutatable objects. This is used my all mutation and crossover methods, in order to retuen a new modified Genome object. 
	 * @param elements The List of Mutatable objects to be added to the newly constructed Genome
	
	 * @return The new Genome object */
	protected abstract Genome<DataType> newInstance(List<Mutatable<DataType>> elements);
	
	/**
	 * Simple constructor for an empty genome with an empty name. 
	 */
	public Genome(){
		this.setName("");
	}
	
	/**
	 * Constructor for Genome.
	 * @param newElements List<Mutatable<DataType>>
	 */
	public Genome(List<Mutatable<DataType>> newElements){
		this.setName("");
		this.setElements(newElements);
	}

	// Mutation Methods //////////////////////////////////////////////
	/**
	 * This method mutates every element of this Genome. Useful for creating an entirely random genome. Randomness depends on the implementation of Mutatable.mutate()
	
	 * @see Mutatable */
	public void mutateAll(){
		for (Mutatable<DataType> element : elements) {
			element.mutate();
		}
	}
	
	/**
	 * Method mutate.
	 * @param probability double
	
	 * @return Genome<DataType> */
	public Genome<DataType> mutate(double probability){
		List<Mutatable<DataType>> newElements = new ArrayList<Mutatable<DataType>>();
		for (Mutatable<DataType> oldElement : elements) {
			Mutatable<DataType> element = oldElement.clone();
			newElements.add(element);
			if(Math.random() <= probability) element.mutate();
		}
		return newInstance(newElements);
	}
	
	/**
	 * Method mutateElement.
	 * @param element Mutatable<DataType>
	
	 * @return Mutatable<DataType> */
	public Mutatable<DataType> mutateElement(Mutatable<DataType> element) {
		return this.mutateElementAtIndex(this.getElements().indexOf(element));
	}
	
	/**
	 * Method mutateElementAtIndex.
	 * @param i int
	
	 * @return Mutatable<DataType> */
	public Mutatable<DataType> mutateElementAtIndex(int i) {
		return this.getElement(i).mutate();
	}
	
	
	// Crossover Methods //////////////////////////////////////////////
	/**
	 * Method singlePointCrossover.
	 * @param genome2 Genome<DataType>
	
	 * @return Genome<DataType> */
	public Genome<DataType> singlePointCrossover(Genome<DataType> genome2){
		return multiPointCrossover(genome2, 1);
	}

	/**
	 * Method twoPointCrossover.
	 * @param genome2 Genome<DataType>
	
	 * @return Genome<DataType> */
	public Genome<DataType> twoPointCrossover(Genome<DataType> genome2){
		return multiPointCrossover(genome2, 2);
	}
	
	/**
	 * Method multiPointCrossover.
	 * @param genome2 Genome<DataType>
	 * @param crosses int
	
	 * @return Genome<DataType> */
	public Genome<DataType> multiPointCrossover(Genome<DataType> genome2, int crosses){
		// Check for length match and that the number of crosses specified is applicable
		int length = verifyCrossoverCompatability(genome2);
		if(crosses > (length - 1)) throw new TooManyCrossesException(crosses);
		
		List<Mutatable<DataType>> newElements= new ArrayList<Mutatable<DataType>>();
		Integer nums[] = new Integer[length-1];
		
		for (int i = 0; i < length-1; i++) { nums[i] = i+1; }
		
		List<Integer> shuffled = Arrays.asList(nums);
		Collections.shuffle(shuffled);
		shuffled = shuffled.subList(0, crosses);
		boolean selectFromParentA = true;
		for(int i = 0; i < length; i++){
			if(shuffled.contains((Integer)i)){
				selectFromParentA = !selectFromParentA;
			}
			if(selectFromParentA) newElements.add(this.getElement(i).clone());
			else newElements.add(genome2.getElement(i).clone());
		}
		
		return newInstance(newElements);
	}
	
	/**
	 * Method uniformCrossover.
	 * @param genome2 Genome<DataType>
	
	 * @return Genome<DataType> */
	public Genome<DataType> uniformCrossover(Genome<DataType> genome2){
		int length = verifyCrossoverCompatability(genome2);
		List<Mutatable<DataType>> newElements= new ArrayList<Mutatable<DataType>>();

		boolean selectFromParentA = true;
		for(int i = 0; i < length; i++){
			if(Math.random() <= 0.5){
				selectFromParentA = !selectFromParentA;
			}
			if(selectFromParentA) newElements.add(this.getElement(i));
			else newElements.add(genome2.getElement(i));
		}

		return newInstance(newElements);
	}
	
	/**
	 * Method elementCrossover.
	 * @param genome2 Genome<DataType>
	
	 * @return Genome<DataType> */
	public Genome<DataType> elementCrossover(Genome<DataType> genome2){
		int length = verifyCrossoverCompatability(genome2);
		List<Mutatable<DataType>> newElements= new ArrayList<Mutatable<DataType>>();
		for(int i = 0; i < length; i++){
			newElements.add(this.getElement(i).elementCrossover(genome2.getElement(i)));
		}

		return newInstance(newElements);
	}
	
	/**
	 * Method verifyCrossoverCompatability.
	 * @param genome2 Genome<DataType>
	
	 * @return int */
	private int verifyCrossoverCompatability(Genome<DataType> genome2) {
		if(this.size() != genome2.size()) throw new GenomeSizeMismatchException("Genome 1: " + this.size() + " Genome 2: " + genome2.size());
		else return this.size();
	}
	
	// Getters and Setters //////////////////////////////////////////////
	/**
	 * Method getElements.
	
	 * @return List<Mutatable<DataType>> */
	public List<Mutatable<DataType>> getElements(){
		return elements;
	}

	/**
	 * Method setElements.
	 * @param arrayList List<Mutatable<DataType>>
	 */
	public void setElements(List<Mutatable<DataType>> arrayList) {
		this.elements = arrayList;
	}
	
	/**
	 * Method getElement.
	 * @param index int
	
	 * @return Mutatable<DataType> */
	public Mutatable<DataType> getElement(int index){
		return this.elements.get(index);
	}
	
	/**
	 * Method setElement.
	 * @param index int
	 * @param element Mutatable<DataType>
	 */
	public void setElement(int index, Mutatable<DataType> element){
		this.elements.set(index, element);
	}
	
	/**
	 * Method getName.
	
	 * @return String */
	public final String getName() {
		return name;
	}

	/**
	 * Method setName.
	 * @param name String
	 */
	public final void setName(String name) {
		this.name = name;
	}
	
	// Utility Methods ////////////////////////////////////////////////
	/**
	 * Method size.
	
	 * @return int */
	public int size(){
		if(elements != null){
			return elements.size();
		}
		else return 0;
	}

	/**
	 * Method toString.
	
	 * @return String */
	@Override
	public String toString(){
		String str = this.getName() + "\n";
		str = str + ("Size: " + this.size() + "\n");
		
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.append(str);
		for(Integer i = 0; i < this.size(); i++){
			String fmt = "%1$" + ((Integer) (this.size()-1)).toString().length() + "d";
			stringBuilder.append((String.format(fmt, i) + ": " + this.getElements().get(i).toString() + "\n"));
		}
		
		return stringBuilder.toString();
	}
	

	// Main (For testing) //////////////////////////////////////////////
	/**
	 * Method main.
	 * @param args String[]
	 */
	public static void main(String [] args){
		int size = 100;
		
		StaticGenome<Integer> genomeA = new StaticGenome<Integer>("Genome A");
		for(int i = 0; i < size; i++){
			genomeA.getElements().add(new IntegerElement(10));
		}
		
		StaticGenome<Integer> genomeB = new StaticGenome<Integer>("Genome B");
		for(int i = 0; i < size; i++){
			genomeB.getElements().add(new IntegerElement(0));
		}
		
		Genome<Integer> offspring = genomeA.uniformCrossover(genomeB);
		offspring = offspring.mutate(0.2);
		offspring = offspring.elementCrossover(genomeB);
		offspring = offspring.mutate(0.2);
		offspring = offspring.twoPointCrossover(genomeA);
		
		offspring.setName("Offspring");
		System.out.println(offspring);
	}
	
}
